package com.hzw.saas.web.hbox.controller;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.hzw.saas.api.storage.IResourceFileService;
import com.hzw.saas.api.storage.IResourceFolderService;
import com.hzw.saas.api.storage.IStorageService;
import com.hzw.saas.api.storage.bo.ResourceFileBO;
import com.hzw.saas.api.storage.bo.ResourceFileStorageBO;
import com.hzw.saas.api.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.storage.enums.ResourceAuthEnum;
import com.hzw.saas.api.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.storage.exception.FileConflictException;
import com.hzw.saas.api.storage.query.ResourceFileUpdateQuery;
import com.hzw.saas.common.config.annotation.SysLog;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.query.LogicQuery;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.security.utils.SecurityUtils;
import com.hzw.saas.common.util.HttpContextUtils;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.enums.AssertEnum;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.common.util.exception.ForbiddenException;
import com.hzw.saas.service.storage.annotation.FileAuthAfter;
import com.hzw.saas.service.storage.annotation.FileAuthBefore;
import com.hzw.saas.service.storage.annotation.FolderAuthBefore;
import com.hzw.saas.service.storage.config.StorageConfig;
import com.hzw.saas.service.storage.util.StorageUtil;
import com.hzw.saas.web.hbox.bo.LocChunkBO;
import com.hzw.saas.web.hbox.dto.HBoxFileDTO;
import com.hzw.saas.web.hbox.param.*;
import com.hzw.saas.web.hbox.service.ILocChunkService;
import com.hzw.saas.web.hbox.util.HBoxUtil;
import io.swagger.annotations.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;

/**
 * 文件控制器
 *
 * @author zzl
 * @since 02/26/2021
 */
// @Controller
@RequiredArgsConstructor
@Slf4j
// @Api(tags = "资源管理/文件管理")
public class HBoxFileController {

    private final StorageConfig storageConfig;
    private final IStorageService storageService;
    private final IResourceFileService fileService;
    private final IResourceFolderService folderService;
    private final ILocChunkService chunkService;

    @GetMapping("/resource/file/{fileId}/info")
    @FileAuthAfter(auth = ResourceAuthEnum.READ)
    @SysLog
    @ApiOperation(value = "获取文件信息", notes = "获取文件的详细信息")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "fileId", value = "文件ID", dataType = "String", required = true)
    })
    @ApiOperationSort(1)
    public ResponseEntity<HBoxFileDTO> getFileInfo(@PathVariable String fileId) {
        ResourceFileBO fileBO = fileService.getFile(fileId,
            LogicQuery.ne(ResourceStatusEnum.DELETED.groupCodes()), LogicQuery.none(), AssertEnum.FOUND);
        HBoxFileDTO result = MapperUtil.nf().map(fileBO, HBoxFileDTO.class);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/resource/file/info")
    @FolderAuthBefore(folderId = "#param.folderId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "提交文件信息", mode = 3)
    @ApiOperation(value = "提交文件信息", notes = "提交文件信息，若存在同名文件，当覆盖模式为false时，会返回409错误，当覆盖模式为true时，会覆盖同名文件")
    @ApiResponses({
        @ApiResponse(code = 200, message = "返回资源标识ID"),
        @ApiResponse(code = 400, message = "请求参数错误"),
        @ApiResponse(code = 403, message = "无访问权限；单文件大小超过1G；存储总容量不足"),
        @ApiResponse(code = 409, message = "存在同名文件"),
    })
    @ApiOperationSort(2)
    public ResponseEntity<String> saveFileInfo(@Validated @RequestBody HBoxFilePostParam param) {
        // 操作用户
        String userId = SecurityUtils.getUser().getUserId();
        param.setFolderId(HBoxUtil.parseFolderId(userId, param.getFolderId()));
        String spaceId = this.getSpaceId(param.getFolderId());
        // 判断文件大小是否超出
        this.assertNotOverLimitSize(param.getLength());
        // 判断用户存储空间是否足够
        storageService.assertNotOverSize(userId, spaceId, param.getLength());
        ResourceFileStorageBO fileBO = new ResourceFileStorageBO();
        fileBO.setCreatorId(userId)
            .setEditorId(userId);
        fileBO.setSupId(param.getFolderId())
            .setName(param.getName());
        fileBO.setLength(param.getLength())
            .setStrongChecksum(param.getDigest());
        fileBO.setCatalog(storageConfig.getPersonalSpaceCatalog());
        fileBO.setIntegrity(FileIntegrityEnum.UPLOADING.getCode());
        try {
            ConflictModeEnum conflictModeEnum = Objects.equals(param.getCover(), true) ? ConflictModeEnum.COVER_TAR_PURE : ConflictModeEnum.THROW_ERR;
            ResourceFileStorageBO result = fileService.saveFile(fileBO, conflictModeEnum);
            return ResponseEntity.ok(result.getPid());
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @GetMapping("/resource/file/{fileId}/chunks/info")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.OWN)
    @SysLog
    @ApiOperation(value = "获取文件分片列表", notes = "获取文件分片列表")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "fileId", value = "文件ID", dataType = "String", required = true)
    })
    @ApiOperationSort(4)
    public ResponseEntity<List<LocChunkBO>> listChunks(@PathVariable String fileId) {
        List<LocChunkBO> fileChunkBOList = this.chunkService.listChunks(fileId, storageConfig.getPersonalSpaceCacheCatalog());
        return ResponseEntity.ok(fileChunkBOList);
    }

    @GetMapping("/resource/file/chunk/info")
    @ApiOperation(value = "分片上传校验", notes = "分片上传校验，检查分片是否已上传")
    @ApiResponses({
        @ApiResponse(code = 200, message = "分片已上传过"),
        @ApiResponse(code = 304, message = "分片未上传过"),
        @ApiResponse(code = 400, message = "请求参数异常/文件信息不存在"),
        @ApiResponse(code = 500, message = "服务器异常")
    })
    @ApiOperationSort(5)
    public ResponseEntity<Void> verifyChunk(@Validated LocChunkGetParam param) {
        boolean exist = this.chunkService.exist(param.getFileId(), storageConfig.getPersonalSpaceCacheCatalog(),
            param.getStartBytes(), param.getEndBytes(), param.getDigest());
        HttpStatus status = exist ? HttpStatus.OK : HttpStatus.NOT_MODIFIED;
        return ResponseEntity.status(status).build();
    }

    @PostMapping(value = "/resource/file/chunk", consumes = {"multipart/form-data"})
    @ApiOperation(value = "分片上传", notes = "分片上传，该接口支持同一分片重复调用上传。上传文件前请先调用<提交文件信息>接口，获取服务器提供的文件ID")
    @ApiResponses({
        @ApiResponse(code = 200, message = "上传成功"),
        @ApiResponse(code = 400, message = "请求参数异常/文件信息不存在/请求类型异常"),
        @ApiResponse(code = 415, message = "文件类型不支持"),
        @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @ApiOperationSort(6)
    public ResponseEntity<Void> saveChunk(@Validated LocChunkPostParam param, HttpServletRequest request) {
        // 判断请求类型
        boolean multipart = ServletUtil.isMultipart(request);
        AssertUtil.assertThrow("上传失败，请求参数异常，请使用multipart/form-data类型上传", !multipart);
        // 获取分片信息和文件
        LocChunkBO locChunkBO = MapperUtil.nf().map(param, LocChunkBO.class);
        byte[] chunkData;
        try {
            chunkData = param.getFile().getBytes();
        } catch (IOException e) {
            throw DefaultSaasException.build("获取分片文件异常" + param.toString());
        }
        // 保存分片
        this.chunkService.saveChunk(storageConfig.getPersonalSpaceCacheCatalog(), locChunkBO, chunkData);
        return ResponseEntity.ok().build();
    }

    @PutMapping("/resource/file/chunks")
    @FileAuthBefore(fileId = "#param.fileId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "完成文件分片上传", mode = 1)
    @ApiOperation(value = "完成文件分片上传", notes = "完成文件分片上传，对文件分片进行合并")
    @ApiOperationSort(7)
    public ResponseEntity<Void> mergeChunks(@Validated @RequestBody HBoxFileMergeParam param) {
        String userId = SecurityUtils.getUser().getUserId();
        if (StrUtil.isNotBlank(param.getFolderId())) {
            param.setFolderId(HBoxUtil.parseFolderId(userId, param.getFolderId()));
            String newName = StrUtil.isBlank(param.getName()) ? null : param.getName();
            fileService.moveFile(param.getFileId(), param.getFolderId(), newName, ConflictModeEnum.COVER_TAR);
        } else if (StrUtil.isNotBlank(param.getName())) {
            fileService.renameFile(param.getFileId(), param.getName(), ConflictModeEnum.COVER_TAR);
        }
        ResourceFileUpdateQuery updateQuery = new ResourceFileUpdateQuery();
        updateQuery.setPid(param.getFileId());
        updateQuery.setLength(param.getLength());
        updateQuery.setStrongChecksum(param.getDigest());
        fileService.updateFile(updateQuery);
        this.chunkService.mergeChunks(param.getFileId(), storageConfig.getPersonalSpaceCacheCatalog(), param.getChunkIdList());
        return ResponseEntity.ok().build();
    }

    @PutMapping("/resource/file/info")
    @FileAuthBefore(fileId = "#param.fileId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "更新文件信息", mode = 3)
    @ApiOperation(value = "更新文件信息", notes = "更新文件信息")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 400, message = "请求参数异常/目录信息不存在"),
        @ApiResponse(code = 409, message = "恢复至的目录存在同名文件"),
        @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @ApiOperationSort(8)
    public ResponseEntity<Void> updateFile(@Validated @RequestBody HBoxFilePutParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            if (StrUtil.isNotBlank(param.getFolderId())) {
                param.setFolderId(HBoxUtil.parseFolderId(userId, param.getFolderId()));
                String newName = StrUtil.isBlank(param.getName()) ? null : param.getName();
                fileService.moveFile(param.getFileId(), param.getFolderId(), newName, ConflictModeEnum.THROW_ERR);
            } else if (StrUtil.isNotBlank(param.getName())) {
                fileService.renameFile(param.getFileId(), param.getName(), ConflictModeEnum.THROW_ERR);
            }
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @PutMapping("/resource/file")
    @FileAuthBefore(fileId = "#param.fileId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "恢复文件", notes = "恢复被回收的文件，若提示父目录已被删除，请指定parentId恢复到其他正常父目录，若提示恢复至的目录存在同名目录，请指定name更新目录名")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 400, message = "请求参数异常/目录信息不存在"),
        @ApiResponse(code = 409, message = "恢复至的目录存在同名文件"),
        @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @ApiOperationSort(9)
    public ResponseEntity<Void> restoreFile(@Validated @RequestBody HBoxFileRestoreParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            param.setFolderId(HBoxUtil.parseFolderId(userId, param.getFolderId()));
            fileService.restoreFile(param.getFileId(), param.getFolderId(), param.getName(), ConflictModeEnum.THROW_ERR);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @DeleteMapping("/resource/file")
    @FileAuthBefore(fileId = "#param.fileId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "回收/删除文件", mode = 3)
    @ApiOperation(value = "回收/删除文件", notes = "回收文件，或彻底删除文件")
    @ApiOperationSort(11)
    public ResponseEntity<Void> deleteFile(@Validated @RequestBody HBoxFileDeleteParam param) {
        if (Objects.equals(ResourceStatusEnum.RECYCLED.getCode(), param.getMode())) {
            fileService.recycleFile(param.getFileId());
        } else if (Objects.equals(ResourceStatusEnum.DELETED.getCode(), param.getMode())) {
            fileService.deleteFile(param.getFileId());
        } else {
            throw DefaultSaasException.build("请求模式异常").status(HttpStatus.BAD_REQUEST);
        }
        return ResponseEntity.ok().build();
    }

    @GetMapping("/resource/file/{fileId}")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.READ)
    @ApiOperation(value = "下载文件", notes = "下载文件")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "access_token", value = "用户token（或在请求头设置Authorization）", dataType = "String", paramType = "query", required = false),
        @ApiImplicitParam(name = "fileId", value = "文件ID", required = true, dataType = "String"),
        @ApiImplicitParam(name = "startBytes", value = "从该字节处开始下载", dataType = "int", paramType = "query")
    })
    @ApiOperationSort(12)
    public void download(@PathVariable String fileId, Long startBytes, HttpServletRequest request, HttpServletResponse response) {
        log.debug("start download file: " + fileId);
        ResourceFileBO resourceFileBo = fileService.getFile(fileId, AssertEnum.FOUND);
        try (OutputStream fos = response.getOutputStream()) {
            startBytes = Objects.isNull(startBytes) || (startBytes.compareTo(0L) <= 0) ? 1L : startBytes;
            log.debug("start download file: {}, start: {}", resourceFileBo.getPid(), startBytes);
            HttpContextUtils.setFileDownloadHeader(request, response, resourceFileBo.getName(), resourceFileBo.getLength() - (startBytes - 1L));
            fileService.downloadFileData(fileId, fos, startBytes);
            response.flushBuffer();
            log.debug("download file over...");
        } catch (IOException e) {
            log.error("download error", e);
            throw DefaultSaasException.build("下载文件异常");
        }
    }

    private String getSpaceId(String folderId) {
        if (StorageUtil.isRootId(folderId))
            return StorageUtil.parseSpaceIdByRootId(folderId);
        if (StorageUtil.isSpaceId(folderId))
            return folderId;
        return folderService.getFolder(folderId, AssertEnum.FOUND).getSpaceId();
    }

    private void assertNotOverLimitSize(long length) {
        if (length > storageConfig.getFileMaxSize()) {
            throw new ForbiddenException("文件大小不可超过1G");
        }
    }
}
